/*
 * Copyright 2011 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package me.l1k3.fx.client.engine.impl;

import java.util.ArrayList;
import java.util.List;
import com.google.gwt.animation.client.AnimationScheduler;
import com.google.gwt.core.client.Duration;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.Timer;

/**
 * taken from AnimationSchedulerImplTimer which is unfortunately a package class
 */
public class AnimationSchedulerJSTimer extends AnimationScheduler {
    
    private static AnimationSchedulerJSTimer INSTANCE;
    
    public static AnimationSchedulerJSTimer get() {
        if(INSTANCE==null) {
            INSTANCE = new AnimationSchedulerJSTimer();
        }
        
        return INSTANCE;
    }
    
    /**
     * Timer based implementation of
     * {@link AnimationScheduler.AnimationHandle}.
     */
    private class AnimationHandleImpl extends AnimationHandle {
        private final AnimationCallback callback;

        public AnimationHandleImpl(AnimationCallback callback) {
            this.callback = callback;
        }

        @Override
        public void cancel() {
            cancelAnimationFrame(this);
        }

        public AnimationCallback getCallback() {
            return callback;
        }
    }

    /**
     * The default time in milliseconds between frames. 60 fps == 16.67 ms.
     */
    private static final int DEFAULT_FRAME_DELAY = 16;

    /**
     * The minimum delay in milliseconds between frames. The minimum delay
     * is imposed to prevent freezing the UI.
     */
    private static final int MIN_FRAME_DELAY = 5;

    /**
     * The list of animations that are currently running.
     */
    private final List<AnimationHandleImpl> animationRequests = new ArrayList<AnimationHandleImpl>();

    /**
     * The singleton timer that updates all animations.
     */
    private final Timer timer = new Timer() {
        @Override
        public void run() {
            updateAnimations();
        }
    };

    @Override
    public AnimationHandle requestAnimationFrame(final AnimationCallback callback, Element element) {
        // Save the animation frame request.
        AnimationHandleImpl requestId = new AnimationHandleImpl(callback);
        animationRequests.add(requestId);

        // Start the timer if it isn't started.
        if (animationRequests.size() == 1) {
            timer.schedule(DEFAULT_FRAME_DELAY);
        }

        // Return the request id.
        return requestId;
    }

    private void cancelAnimationFrame(AnimationHandle requestId) {
        // Remove the request from the list.
        animationRequests.remove(requestId);

        // Stop the timer if there are no more requests.
        if (animationRequests.size() == 0) {
            timer.cancel();
        }
    }

    /**
     * Iterate over all animations and update them.
     */
    private void updateAnimations() {
        // Copy the animation requests to avoid concurrent modifications.
        AnimationHandleImpl[] curAnimations = new AnimationHandleImpl[animationRequests.size()];
        curAnimations = animationRequests.toArray(curAnimations);

        // Iterate over the animation requests.
        Duration duration = new Duration();
        for (AnimationHandleImpl requestId : curAnimations) {
            // Remove the current request.
            animationRequests.remove(requestId);

            // Execute the callback.
            requestId.getCallback().execute(duration.getStartMillis());
        }
        
        // Reschedule the timer if there are more animation requests.
        if (animationRequests.size() > 0) {
            /*
             * In order to achieve as close to 60fps as possible, we
             * calculate the new delay based on the execution time of this
             * method. The delay will be less than 16ms, assuming this
             * method takes more than 1ms to complete.
             */
            timer.schedule(Math.max(MIN_FRAME_DELAY, DEFAULT_FRAME_DELAY - duration.elapsedMillis()));
        }
    }
}
